/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.mapping;

import java.awt.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.text.*;
import org.jdesktop.beansbinding.*;
import org.jdesktop.beansbinding.AutoBinding.*;
import org.jdesktop.beansbinding.Binding.*;
import org.jdesktop.swingbinding.*;
import ch.trackedbean.binding.beanSpec.*;
import ch.trackedbean.binding.guicomponents.*;
import ch.trackedbean.binding.guicomponents.lists.*;
import ch.trackedbean.loaders.lists.*;
import ch.trackedbean.util.*;
import ch.trackedbean.validator.*;

/**
 * This group manages the mappings of a single Bean.<br>
 * Consider using the {@link DataPanel}.
 * 
 * @param <T> The type of the bean used as model.
 * @author M. Hautle
 */
@SuppressWarnings("unchecked")
public class MappingGroup<T> {
	/** The bindings */
	protected final BindingGroup grp = new BindingGroup();

	/** List holding unmanaged bindings. */
	protected final List<Binding> unmanagedBindings = new ArrayList<Binding>();

	/** Map containing informations for the bindings in this group */
	protected final IdentityHashMap<Binding, BindingInfos> bindingInfos = new IdentityHashMap<Binding, BindingInfos>();

	/** The binding listener used for validation purposes. */
	protected final ValidationListener validationListener = new ValidationListener();

	/** The list of validators */
	protected final ArrayList<IGroupValidator> validators = new ArrayList<IGroupValidator>();

	/** Flag inidcating if the properties should be validated when they change. */
	protected boolean propertyValidation;

	/** Flag indicating if the mapping group is read only */
	protected final boolean readOnly;

	/** The mapped bean */
	protected T model;

	/** The status of {@link #model}. */
	protected IBeanStatus status;

	/**
	 * Default constructor
	 * 
	 * @param readOnly True if the group bindings are just read only
	 */
	public MappingGroup(final boolean readOnly) {
		this.readOnly = readOnly;
		grp.addBindingListener(validationListener);
	}

	/**
	 * Sets the given model.<br>
	 * If null is passed {@link #removeModel()} is called.<br>
	 * If the group is already bound to a model then the group will be rebound to the new model.
	 * 
	 * @param model The model to set if null is passed {@link #removeModel()} is called
	 * @param validate True if the property validation should be activated
	 */
	public void setModel(final T model, boolean validate) {
		if (model == null) {
			removeModel();
			return;
		}
		// first unbind the current model
		if (this.model != null)
			removeModel();
		bind(model);
		propertyValidation = validate;
	}

	/**
	 * Removes the current model
	 */
	public void removeModel() {
		// unbind the managed bindings
		unbind(grp.getBindings());
		// unbind the unmanaged bindings
		unbind(unmanagedBindings);
		// unbind the sub bindings
		for (BindingInfos b : bindingInfos.values())
			b.unbindSubBinding();
		model = null;
		status = null;
	}

	/**
	 * Returns the model of this mapping group
	 * 
	 * @return The model of this group
	 */
	public T getModel() {
		return model;
	}

	/**
	 * Returns the bean status of the mapped bean
	 * 
	 * @return The bean status or null
	 */
	public IBeanStatus getModelStatus() {
		return status;
	}

	/**
	 * Binds the model
	 * 
	 * @param model The model to bind
	 */
	private void bind(final T model) {
		// bind the managed bindings
		for (final Binding b : grp.getBindings()) {
			final BindingInfos infos = bindingInfos.get(b);
			infos.configure(b);
			infos.bindSubBinding();
			b.setSourceObject(model);
			b.bind();
		}
		// bind the unmanaged bindings
		for (final Binding b : unmanagedBindings) {
			b.setSourceObject(model);
			b.bind();
		}
		this.model = model;
		this.status = BeanStatusImpl.getBeanStatus(model);
	}

	/**
	 * Unbinds the given bindings.<br>
	 * The value of the targetproperty will be set to null if it implements {@link ICleanUp} or if <code>cleanUp</code> is <code>true</code>.
	 * 
	 * @param bindings The bindings
	 */
	private void unbind(Collection<? extends Binding> bindings) {
		for (final Binding b : bindings) {
			if (b.isBound())
				b.unbind();
			final Object target = b.getTargetObject();
			if (target instanceof ICleanUp)
				b.getTargetProperty().setValue(target, null);
			b.setSourceObject(null);
		}
	}

	/**
	 * Validates all bound properties.
	 */
	public void validateAllProperties() {
		if (model == null)
			return;
		for (Binding b : grp.getBindings())
			validationListener.validateProperty(b);
	}

	/**
	 * Creates a binding between two beans and adds it to the group.<br>
	 * 
	 * @param srcPath The source path
	 * @param dst The destination bean
	 * @param dstPath The destination path
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String srcPath, final Object dst, final String dstPath) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, srcPath, dst, dstPath, readOnly);
		final BindingInfos i = new BindingInfos(srcPath);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the status of the source property and the desination property<br>
	 * Use {@link #createUnmanagedStatusBinding(String, Object, String)} if you don't need to configure the binding.
	 * 
	 * @param srcPath The source path (where the {@link Status} is taken from)
	 * @param dst The destination bean
	 * @param dstPath The destination path
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createStatusBinding(final String srcPath, final Object dst, final String dstPath) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createStatusBinding(model, srcPath, dst, dstPath);
		final BindingInfos i = new BindingInfos(srcPath);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a normal binding between the status of the source property and the desination property.<br>
	 * Use {@link #createStatusBinding(String, Object, String)} if you need to configure the binding.
	 * 
	 * @param srcPath The source path (where the {@link Status} is taken from)
	 * @param dst The destination bean
	 * @param dstPath The destination path
	 * @throws IllegalStateException if the group is already bound
	 */
	public void createUnmanagedStatusBinding(final String srcPath, final Object dst, final String dstPath) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		unmanagedBindings.add(MappingUtils.createStatusBinding(model, srcPath, dst, dstPath));
	}

	/**
	 * Creates a binding between the status of the property and the element.<br>
	 * 
	 * @param <E> The type of the element
	 * @param srcPath The source path (where the status is taken from)
	 * @param element The target element
	 * @return The passed element
	 * @throws IllegalStateException if the group is already bound
	 */
	public <E extends IStatusDependent> E createStatusDependentBinding(final String srcPath, final E element) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		unmanagedBindings.add(MappingUtils.createStatusActionBinding(model, srcPath, element));
		return element;
	}

	/**
	 * Binds the given {@link IDependentElement} to the given property.
	 * 
	 * @param <E> The type of the element
	 * @param path The path to the property to bind
	 * @param element The element to bind
	 * @return The passed element
	 */
	public <E extends IDependentElement> E createDependentElementBinding(String path, E element) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		unmanagedBindings.add(MappingUtils.createDependentActionBinding(model, path, element));
		return element;
	}

	/**
	 * Binds the given {@link IListAdapter} to the given property.
	 * 
	 * @param <C> The adapter component type
	 * 
	 * @param path The path to the property to bind
	 * @param adapter The adapter to bind
	 * @return The passed adapter
	 */
	public <C extends Component> IListAdapter<C, ?> createListAdapterBinding(String path, IListAdapter<C, ?> adapter) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		unmanagedBindings.add(MappingUtils.createListAdapterBinding(model, path, adapter));
		return adapter;
	}

	/**
	 * Creates a binding between the given property and the given field and adds it to the group.<br>
	 * 
	 * @param path The path to the property
	 * @param field The {@link JTextComponent} to link
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String path, final JTextComponent field) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, path, field, readOnly);
		final BindingInfos i = new BindingInfos(path);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the given property and the given label and adds it to the group.<br>
	 * 
	 * @param path The path to the property
	 * @param label The JLabel to link
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String path, final JLabel label) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, path, label);
		final BindingInfos i = new BindingInfos(path);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the given property and the given combobox and adds it to the group.<br>
	 * Consider using {@link #createBinding(String, JComboBox, Class, String, Map)}.
	 * 
	 * @param path The path to the property
	 * @param combobox The {@link JComboBox} to link
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String path, final JComboBox combobox) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, path, combobox, readOnly);
		final BindingInfos i = new BindingInfos(path);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the given property and the given list and adds it to the group.<br>
	 * 
	 * @param path The path to the property
	 * @param list The {@link JList} to link
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String path, final JList list) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, path, list);
		final BindingInfos i = new BindingInfos(path);
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the given property and the given combobox and adds it to the group.<br>
	 * The combobox content list will be loaded using {@link ListManager#getList(Class, String, Map)}.
	 * 
	 * @param path The path to the property
	 * @param combobox The {@link JComboBox} to link
	 * @param type The content type of the combobox list
	 * @param subType Additional string specifying the "subtype" of the combobox content list (may be null)
	 * @param settings Additional settings for the combobox content load (may be null)
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @see ListManager#getList(Class, String, Map)
	 * @see ListManager#registerLoader(Class, String, IListLoader)
	 * @throws IllegalStateException if the group is already bound
	 */
	public BindingInfos createBinding(final String path, final JComboBox combobox, Class<?> type, String subType, Map<Object, Object> settings) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		final Binding b = MappingUtils.createBinding(model, path, combobox, readOnly);
		final JComboBoxBinding<Object, List<Object>, JComboBox> listBinding = SwingBindings.createJComboBoxBinding(UpdateStrategy.READ, null, combobox);
		final BindingInfos i = new BindingInfos(path);
		i.setSubBinding(new ListBindingInfos(listBinding, type, subType, settings));
		bindingInfos.put(b, i);
		grp.addBinding(b);
		return i;
	}

	/**
	 * Creates a binding between the given property and the table mapping.<br>
	 * Use this binding if your bean contains a property holding a list which should be displayed using a table.
	 * 
	 * @param path The path to the property
	 * @param table The table mapping to bind
	 * @return The {@linkBindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is called)
	 */
	public BindingInfos createTableBinding(final String path, TableMapping table) {
		return createBinding(path, table, TableMapping.MODEL_PROPERTY);
	}

	/**
	 * Adds the given binding<br>
	 * Please use this method only if it's necessary!<br>
	 * -The binding to add has to be unbound!<br>
	 * -The source object of the binding will be overriden by this group<br>
	 * 
	 * @param path The path of the property bound in the given binding
	 * @param binding The binding to add
	 * @return The {@link BindingInfos} object for further configuration of the binding (they will be activated when {@link #setModel(Object,boolean)} is
	 *         called)
	 * @throws IllegalStateException if the binding or the group is already bound
	 * @throws IllegalArgumentException if the binding has no name
	 */
	public BindingInfos createBinding(final String path, final Binding binding) {
		if (model != null)
			throw new IllegalStateException("Can't add a binding if the group is already bound");
		if (binding.isBound())
			throw new IllegalStateException("Binding is already bound!");
		final BindingInfos i = new BindingInfos(path);
		bindingInfos.put(binding, i);
		grp.addBinding(binding);
		return i;
	}

	/**
	 * Adds a group validator
	 * 
	 * @param validator The validator to add
	 */
	public void addGroupValidator(final IGroupValidator<T> validator) {
		validators.add(validator);
	}

	/**
	 * Removes the group validator
	 * 
	 * @param validator The validator to add
	 */
	public void removeGroupValidator(final IGroupValidator validator) {
		validators.remove(validator);
	}

	/**
	 * {@link BindingListener} used to notify {@link IGroupValidator}s if a property has changed.
	 * 
	 * @author M. Hautle
	 */
	private class ValidationListener implements BindingListener {
		/**
		 * Not implemented
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#bindingBecameBound(org.jdesktop.beansbinding.Binding)
		 */
		public void bindingBecameBound(final Binding binding) {
		}

		/**
		 * Not implemented
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#bindingBecameUnbound(org.jdesktop.beansbinding.Binding)
		 */
		public void bindingBecameUnbound(final Binding binding) {
		}

		/**
		 * Not implemented
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#sourceChanged(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.PropertyStateEvent)
		 */
		public void sourceChanged(final Binding binding, final PropertyStateEvent event) {
		}

		/**
		 * Sets the error flag is a conversion fails
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#syncFailed(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.Binding.SyncFailure)
		 */
		public void syncFailed(final Binding binding, final SyncFailure failure) {
			// ignore status properties (doesen't make sense to watch them..)
			if (model == null || binding.getSourceProperty() instanceof StatusProperty)
				return;
			String property = bindingInfos.get(binding).getProperty();
			IBeanStatus status = MappingGroup.this.status;
			// if it's a nested property then get the corresponding status object
			final int lastIndex = property.lastIndexOf('.');
			if (lastIndex > 0)
				try {
					status = BeanStatusImpl.getBeanStatus(model, property.substring(0, lastIndex));
					property = property.substring(lastIndex + 1);
				} catch (final Exception e) {
					e.printStackTrace();
					return;
				}
			if (status != null)
				status.setStatus(property, BeanStatusFlag.TECHNICAL_ERROR, failure.getConversionException().getMessage());
		}

		/**
		 * Informs the validators about changes
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#synced(org.jdesktop.beansbinding.Binding)
		 */
		public void synced(final Binding binding) {
			validateProperty(binding);
		}

		/**
		 * Validates the source property of the given binding.
		 * 
		 * @param binding The binding to validate
		 */
		public void validateProperty(final Binding binding) {
			// ignore StatusProperies (otherwise you get a StackOverflow...)
			if (model == null || binding.getSourceProperty() instanceof StatusProperty)
				return;
			final String src = bindingInfos.get(binding).getProperty();
			validateProperty(src);
			for (final IGroupValidator v : validators)
				v.validate(model, status, src);
		}

		/**
		 * Validates the given property of the model.
		 * 
		 * @param property The property
		 */
		private void validateProperty(String property) {
			// execute the property validation
			if (!propertyValidation)
				return;
			Object bean = model;
			IBeanStatus status = MappingGroup.this.status;
			// if it's a nested property then get the "end" object
			final int lastIndex = property.lastIndexOf('.');
			if (lastIndex > 0)
				try {
					final String path = property.substring(0, lastIndex);
					bean = ELHelper.getValueOf(path, model, false);
					status = BeanStatusImpl.getBeanStatus(model, path);
					property = property.substring(lastIndex + 1);
				} catch (final Exception e) {
					e.printStackTrace();
					return;
				}
			final IErrorDescription[] err = ValidatorManager.validate(property, bean);
			// if there was an error then we will set the error flag
			if (err != null)
				status.setStatus(property, BeanStatusFlag.ERROR, ValidatorManager.buildErrorString(err));
		}

		/**
		 * Not implemented
		 * 
		 * @see org.jdesktop.beansbinding.BindingListener#targetChanged(org.jdesktop.beansbinding.Binding, org.jdesktop.beansbinding.PropertyStateEvent)
		 */
		public void targetChanged(final Binding binding, final PropertyStateEvent event) {
		}
	}
}
